All files / remote online_state_tracker.ts

100% Statements 53/53
100% Branches 14/14
100% Functions 9/9
100% Lines 51/51
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173                                2x 2x 2x 2x     2x       2x           2x                         2x   591x             591x             591x             591x     591x 591x                 410x 410x   410x 392x       4x 4x       4x         4x 4x           4x                       2x 58x 14x   44x 44x 24x 24x 24x                       2x 3521x 3521x   3521x     2280x     3521x     2x 3973x 710x 710x       2x 28x 6x 6x       2x 3545x 388x 388x     2x  
/**
 * Copyright 2018 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
import { OnlineState } from '../core/types';
import * as log from '../util/log';
import { assert } from '../util/assert';
import { AsyncQueue, TimerId } from '../util/async_queue';
import { CancelablePromise } from '../util/promise';
 
const LOG_TAG = 'OnlineStateTracker';
 
// To deal with transient failures, we allow multiple stream attempts before
// giving up and transitioning from OnlineState.Unknown to Offline.
const MAX_WATCH_STREAM_FAILURES = 2;
 
// To deal with stream attempts that don't succeed or fail in a timely manner,
// we have a timeout for OnlineState to reach Online or Offline.
// If the timeout is reached, we transition to Offline rather than waiting
// indefinitely.
const ONLINE_STATE_TIMEOUT_MS = 10 * 1000;
 
/**
 * A component used by the RemoteStore to track the OnlineState (that is,
 * whether or not the client as a whole should be considered to be online or
 * offline), implementing the appropriate heuristics.
 *
 * In particular, when the client is trying to connect to the backend, we
 * allow up to MAX_WATCH_STREAM_FAILURES within ONLINE_STATE_TIMEOUT_MS for
 * a connection to succeed. If we have too many failures or the timeout elapses,
 * then we set the OnlineState to Offline, and the client will behave as if
 * it is offline (get()s will return cached data, etc.).
 */
export class OnlineStateTracker {
  /** The current OnlineState. */
  private state = OnlineState.Unknown;
 
  /**
   * A count of consecutive failures to open the stream. If it reaches the
   * maximum defined by MAX_WATCH_STREAM_FAILURES, we'll set the OnlineState to
   * Offline.
   */
  private watchStreamFailures = 0;
 
  /**
   * A timer that elapses after ONLINE_STATE_TIMEOUT_MS, at which point we
   * transition from OnlineState.Unknown to OnlineState.Offline without waiting
   * for the stream to actually fail (MAX_WATCH_STREAM_FAILURES times).
   */
  private onlineStateTimer: CancelablePromise<void> | null = null;
 
  /**
   * Whether the client should log a warning message if it fails to connect to
   * the backend (initially true, cleared after a successful stream, or if we've
   * logged the message already).
   */
  private shouldWarnClientIsOffline = true;
 
  constructor(
    private asyncQueue: AsyncQueue,
    private onlineStateHandler: (onlineState: OnlineState) => void
  ) {}
 
  /**
   * Called by RemoteStore when a watch stream is started.
   *
   * It sets the OnlineState to Unknown and starts the onlineStateTimer
   * if necessary.
   */
  handleWatchStreamStart(): void {
    this.setAndBroadcast(OnlineState.Unknown);
 
    if (this.onlineStateTimer === null) {
      this.onlineStateTimer = this.asyncQueue.enqueueAfterDelay(
        TimerId.OnlineStateTimeout,
        ONLINE_STATE_TIMEOUT_MS,
        () => {
          this.onlineStateTimer = null;
          assert(
            this.state === OnlineState.Unknown,
            'Timer should be canceled if we transitioned to a different state.'
          );
          log.debug(
            LOG_TAG,
            `Watch stream didn't reach online or offline within ` +
              `${ONLINE_STATE_TIMEOUT_MS}ms. Considering client offline.`
          );
          this.logClientOfflineWarningIfNecessary();
          this.setAndBroadcast(OnlineState.Offline);
 
          // NOTE: handleWatchStreamFailure() will continue to increment
          // watchStreamFailures even though we are already marked Offline,
          // but this is non-harmful.
 
          return Promise.resolve();
        }
      );
    }
  }
 
  /**
   * Updates our OnlineState as appropriate after the watch stream reports a
   * failure. The first failure moves us to the 'Unknown' state. We then may
   * allow multiple failures (based on MAX_WATCH_STREAM_FAILURES) before we
   * actually transition to the 'Offline' state.
   */
  handleWatchStreamFailure(): void {
    if (this.state === OnlineState.Online) {
      this.setAndBroadcast(OnlineState.Unknown);
    } else {
      this.watchStreamFailures++;
      if (this.watchStreamFailures >= MAX_WATCH_STREAM_FAILURES) {
        this.clearOnlineStateTimer();
        this.logClientOfflineWarningIfNecessary();
        this.setAndBroadcast(OnlineState.Offline);
      }
    }
  }
 
  /**
   * Explicitly sets the OnlineState to the specified state.
   *
   * Note that this resets our timers / failure counters, etc. used by our
   * Offline heuristics, so must not be used in place of
   * handleWatchStreamStart() and handleWatchStreamFailure().
   */
  set(newState: OnlineState): void {
    this.clearOnlineStateTimer();
    this.watchStreamFailures = 0;
 
    if (newState === OnlineState.Online) {
      // We've connected to watch at least once. Don't warn the developer
      // about being offline going forward.
      this.shouldWarnClientIsOffline = false;
    }
 
    this.setAndBroadcast(newState);
  }
 
  private setAndBroadcast(newState: OnlineState): void {
    if (newState !== this.state) {
      this.state = newState;
      this.onlineStateHandler(newState);
    }
  }
 
  private logClientOfflineWarningIfNecessary(): void {
    if (this.shouldWarnClientIsOffline) {
      log.error('Could not reach Firestore backend.');
      this.shouldWarnClientIsOffline = false;
    }
  }
 
  private clearOnlineStateTimer(): void {
    if (this.onlineStateTimer !== null) {
      this.onlineStateTimer.cancel();
      this.onlineStateTimer = null;
    }
  }
}